#!/bin/sh
# \
exec tclsh "$0" ${1+"$@"}

######################################################
# TODO Manager
#
# Description: A simple TODO.TXT manager, written in Tcl.
#              Based on a program by Gina Trapani at 
#              http://tinyurl.com/hfn5u.
#
# $Id$
#
# Author: Blair Kitchen (blair@the-blair.com)
# Release Date: 07-Jan-2007
# License: GPL, http://www.gnu.org/copyleft/gpl.html
# Version: 1.2
######################################################

# Package information
package provide app-todo 1.0

# Package dependencies
package require Tcl 8.4

# Check for the TODO_DIR environment variable
if {![info exists env(TODO_DIR)]} {
   puts stderr "Please set the TODO_DIR environment variable to point"
   puts stderr "to a directory that will contain all TODO related files."
   exit -1
}

# Location of the TODO files
set todoDir "$env(TODO_DIR)"
set todoFile [file join $todoDir todo.txt]
set doneFile [file join $todoDir done.txt]
set reportFile [file join $todoDir report.txt]

# Usage message
set usageMessage {Usage: $0 [ACTION] [OPTION]...

Actions:
  add "THING I NEED TO DO p:project @context"
    Adds TODO ITEM to your todo.txt.
    Project and context notation optional.
    Quotes optional.

  append NUMBER "TEXT TO APPEND"
    Adds TEXT TO APPEND to the end of the todo on line NUMBER.
    Quotes optional.

  archive
    Moves done items from todo.txt to done.txt.

  del NUMBER
    Deletes the item on line NUMBER in todo.txt.

  do NUMBER
    Marks item on line NUMBER as done in todo.txt.

  list [TERM] [[TERM]...]
    Displays all todo's that contain TERM(s) sorted by priority with line
    numbers.  If no TERM specified, lists entire todo.txt.

  listpri [PRIORITY]
    Displays all items prioritized PRIORITY.
    If no PRIORITY specified, lists all prioritized items.

  prepend NUMBER "TEXT TO PREPEND"
    Adds TEXT TO PREPEND to the beginning of the todo on line NUMBER.
    Quotes optional.

  pri NUMBER PRIORITY
    Adds PRIORITY to todo on line NUMBER.  If the item is already 
    prioritized, replaces current priority with new PRIORITY.
    PRIORITY must be an uppercase letter between A and Z.

  replace NUMBER "UPDATED TODO"
    Replaces todo on line NUMBER with UPDATED TODO.

  report
    Adds the number of open todo's and closed done's to report.txt.

  More on Lifehacker.com's todo.txt/done.txt manager at
  http://tinyurl.com/hfn5u
  Version 1.1
  Copyleft 2006, Blair Kitchen (blair@the-blair.com)
}

# Function to add a todo item
proc cmd_add {args} {
   set todo [join $args]
   set number [file_write_task $::todoFile 0 $todo]

   puts "'$todo' added on line $number."
}

# Alias for the add command
proc cmd_a {args} {
   eval cmd_add $args
}

# Function to append text to a todo item
proc cmd_append {number args} {
   set text [join $args]
   set todo "[file_read_task $::todoFile $number] $text"
   file_write_task $::todoFile $number $todo

   puts "$number: $todo"
}

# Alias for the append command
proc cmd_app {number args} {
   eval cmd_append $number $args
}

# Function to prepend text to a todo item
proc cmd_prepend {number args} {
   set text [join $args]
   set todo "$text [file_read_task $::todoFile $number]"
   file_write_task $::todoFile $number $todo
   
   puts "$number: $todo"
}

# Alias for the prepend command
proc cmd_prep {number args} {
   eval cmd_prepend $number $args
}

# Function to delete a todo item
proc cmd_del {number} {
   set lineList [file_read $::todoFile]
   if {[llength $lineList] < $number} {
      return -code error "Unknown task: $number"
   }

   set number [expr {$number-1}]
   set todo [lindex $lineList $number]
   file_write $::todoFile [lreplace $lineList $number $number]

   puts "'$todo' deleted."
}

# Alias for the del command
proc cmd_rm {number} {
   cmd_del $number
}

# Function to mark an item as done
proc cmd_do {number} {
   set todo [file_read_task $::todoFile $number]
   set todo [regsub -- {^\([A-Z]\)\s*} $todo ""]
   file_write_task $::todoFile $number "x $todo"

   puts "$number: $todo"
   puts "$number marked as done."
}

# Function to replace a todo
proc cmd_replace {number args} {
   set todo [join $args]
   file_write_task $::todoFile $number $todo

   puts "$number: $todo"
}

# Function to set priority
proc cmd_pri {number priority} {
   set priority [normalize_priority $priority]

   set todo [file_read_task $::todoFile $number]
   
   if {[regsub -- {^\([A-Z]\)} $todo "($priority)" todo] == 0} {
      set todo "($priority) $todo"
   }

   file_write_task $::todoFile $number $todo

   puts "$number: $todo"
   puts "$number prioritized ($priority)"
}

# Function to list todo items, sorted by priority
proc cmd_list {args} {
   set i 1
   set lineList {}
   foreach line [file_read $::todoFile] {
      if {[match_filter $line $args]} {
         lappend lineList [list $i $line]
      }

      incr i
   }

   set lineList [lsort -index 1 $lineList]

   foreach line $lineList {
      puts "[lindex $line 0]: [lindex $line 1]"
   }
}

# Alias for the list command
proc cmd_ls {args} {
   eval cmd_list $args
}

# Function to list todo's with a given priority
proc cmd_listpri {{priority {}}} {
   if {$priority ne {}} {
      set priority "\\([normalize_priority $priority]\\)"
   } else {
      set priority {^\([A-Za-z]{1}\)}
   }

   cmd_list $priority
}

# Alias for the listpri command
proc cmd_lsp {{priority {}}} {
   cmd_listpri $priority
}

# Function to archive completed todos.
proc cmd_archive {} {
   set completeList {}
   set todoList {}

   foreach line [file_read $::todoFile] {
      if {[regexp -- {^x } $line]} {
         puts $line
         lappend completeList $line
      } else {
         lappend todoList $line
      }
   }

   file_write $::todoFile $todoList
   file_write $::doneFile $completeList 1

   puts --
   puts "Items marked as done have been moved from $::todoFile to $::doneFile."
}

# Function to generate report data
proc cmd_report {} {
   set doneCount [llength [file_read $::doneFile]]
   set todoCount [llength [file_read $::todoFile]]
   set dateString [clock format [clock seconds] -format "%Y-%m-%d-%H:%M:%S"]

   set reportLines [file_read $::reportFile]
   if {[llength $reportLines] == 0} {
      lappend reportLines "datetime todos dones"
   }

   lappend reportLines "$dateString $todoCount $doneCount"
   file_write $::reportFile $reportLines

   puts "Report file updated."
   foreach line $reportLines {
      puts $line
   }
}

# Helper function to perform filter matching
proc match_filter {todo filterList} {
   set result 1

   foreach filter $filterList {
      if {![regexp -- $filter $todo]} {
         set result 0
         break
      }
   }

   return $result
}

# Helper function to normalize a priority
proc normalize_priority {priority} {
   if {![regexp -- {^[A-Za-z]{1}$} $priority]} {
      return -code error "PRIORITY must be a letter A-Z"
   }

   return [string toupper $priority]
}

# Helper function to read a file as a list of lines
proc file_read {todoFile} {
   set inChannel [open $todoFile r]
   set text [split [read $inChannel] \n]
   close $inChannel

   set result {}
   foreach line $text {
      if {$line ne ""} {
         lappend result $line
      }
   }

   return $result
}

# Helper function to write a file as a list of lines
proc file_write {todoFile lineList {append 0}} {
   if {$append} {
      set openFlag a
   } else {
      set openFlag w
   }

   set outChannel [open $todoFile $openFlag]
   puts $outChannel [join $lineList \n]
   close $outChannel
}

# Helper function to write a task to a file
# A number of 0 indicates append
# Returns the number of the task written
proc file_write_task {todoFile number task} {
   set lineList [file_read $todoFile]

   if {$number == 0} {
      lappend lineList $task
      set result [llength $lineList]
   } else {
      if {[llength $lineList] < $number} {
         return -code error "Unknown task: $number"
      }
      lset lineList [expr {$number-1}] $task
      set result $number
   }

   file_write $todoFile $lineList

   return $result
}

# Helper function to read a task from a file
proc file_read_task {todoFile number} {
   set lineList [file_read $todoFile]

   if {[llength $lineList] < $number} {
      return -code error "Unknown task: $number"
   }

   return [lindex $lineList [expr {$number-1}]]
}

# Helper function to test for file validity.  Creates the file
# if it does not exist and is able.
proc file_valid {fileName} {
   if {[file exists $fileName]} {
      if {![file readable $fileName] || ![file writable $fileName]} {
         return -code error "cannot read or write $fileName"
      }
   } else {
      if {[catch {open $fileName w} result]} {
         return -code error $result
      }

      close $result
   }
}

# Turn on / off debug messages
interp alias {} debug {} puts
#proc debug {args} {}

# Perform sanity checking on files
foreach fileName [list $todoFile $reportFile $doneFile] {
   if {[catch {file_valid $fileName} result]} {
      puts $result
      debug $errorInfo
      exit -1
   }
}

# Check argument length
if {[llength $argv] == 0} {
   puts $usageMessage
   exit -1
}

set cmdName "cmd_[string tolower [lindex $argv 0]]"
set argv [lrange $argv 1 end]

# Check for valid command
if {[info command $cmdName] == {}} {
   puts $usageMessage
   exit -1
}

if {[catch {eval $cmdName $argv} result]} {
   puts "$result"
   debug $errorInfo
}
